# -*- coding: utf8 -*-
"""
    文件分发
"""

import os
import sys
import time
import socket
import random
import copy
import pickle
import collections
import libtorrent as lt
import threading
from multiprocessing import Pipe
from functools import reduce
from optparse import OptionParser

import logging
logger = logging.getLogger('bit_torrent')
from base.settings import DAEMON_RESOURCE_DISTRIBUTE_PID_FILE, BIT_TORRENT_SETTINGS, FOLDER_CATEGORY_PATH_DICT,\
    TORRENT_FOLDER_ROOT, DAEMON_CHECK_INTERVAL, TORRENT_SUFFIX
from common.daemon import Daemon
from common.debug import print_obj
from common.share_mem import ShareMem
from common.inner_services import InnerServices, InnerAPIError
from common.net_utils import get_wan_ips
from .exceptions import BitTorrentError
from .folder_monitor import FolderMonitor


VERSION = 1.0
CLIENT_INFO = 'bt_server:{} libtorrent:{}'.format(VERSION, lt.version)


def add_suffix(val):
    prefix = ['B', 'kB', 'MB', 'GB', 'TB']
    for i in range(len(prefix)):
        if abs(val) < 1000:
            if i == 0:
                return '%3.3g%s' % (val, prefix[i])
            else:
                return '%2.3g%s' % (val, prefix[i])
        val /= 1000

    return '%6.3gPB' % val

def get_categroy(save_path):
    # print('====> {}'.format(save_path))
    save_path = os.path.abspath(save_path)
    for category, path in FOLDER_CATEGORY_PATH_DICT.items():
        path = os.path.abspath(path)
        if save_path == path:
            return category
    return 'Unknow'

def get_torrent_file_name(name):
    return name + TORRENT_SUFFIX


class ResourceDistributeDaemon(Daemon):
    state_str = [
        'queued', 'checking', 'downloading metadata', 'downloading',
        'finished', 'seeding', 'allocating', 'checking fastresume'
    ]
    # 默认libtorrent设置 参考 https://www.libtorrent.org/reference-Settings.html#overview
    default_settings = {
        'user_agent': CLIENT_INFO,
        'alert_mask': lt.alert.category_t.all_categories,
        'listen_system_port_fallback': False,  # 当端口绑定失败 禁止使用系统默认分配的端口
    }

    @classmethod
    def get_torrent_file_path(cls, file_path, torrent_folder):
        """获取文件对应的torrent文件
        
        Arguments:
            file_path {[type]} -- [description]
            torrent_folder {[type]} -- [description]
        
        Returns:
            string -- torrent文件
        """

        torrent_folder = os.path.abspath(torrent_folder)
        file_name = os.path.split(file_path)[-1]
        torrent_file_name = file_name + TORRENT_SUFFIX
        torrent_file_path = os.path.join(torrent_folder, torrent_file_name)

        return torrent_file_path

    def __init__(self):
        super(ResourceDistributeDaemon, self).__init__(
            name='资源发布进程', pid_file=DAEMON_RESOURCE_DISTRIBUTE_PID_FILE)
        self._net_interface = None
        self._net_interface_port = 6888
        self._settings = {}
        self._torrents = collections.OrderedDict()
        self._resource_peers = collections.OrderedDict()
        self._tracker_urls = []
        self._session = None
        self._sm = ShareMem(access=ShareMem.SHARE_MEM_ACCESS_WRITE)  # 内存共享
        self.settings = BIT_TORRENT_SETTINGS

    @property
    def settings(self):
        return self._settings

    @settings.setter
    def settings(self, value):
        # 参考 https://www.libtorrent.org/reference-Settings.html#overview
        self._settings = copy.deepcopy(self.default_settings)
        # 设置本地监听地址
        if value.get('listen_interfaces'):
            self._net_interface = value['listen_interfaces'].split(':')[0]
            self._net_interface_port = int(
                value['listen_interfaces'].split(':')[1])

        # 设置最大上传速度
        if value.get('upload_rate_limit', -1) > 0:
            self._settings[
                'upload_rate_limit'] = value['upload_rate_limit'] * 1024

        # 设置最大下载速度
        if value.get('download_rate_limit', -1) > 0:
            self._settings[
                'download_rate_limit'] = value['download_rate_limit'] * 1024

        # 设置最大链接数
        if value.get('connections_limit'):
            self._settings['connections_limit'] = value['connections_limit']

        # 设置tracker_backoff
        if value.get('tracker_backoff'):
            self._settings['tracker_backoff'] = value['tracker_backoff']

        # 设置 http 代理
        if value.get('proxy_host'):
            self._settings['proxy_type'] = lt.proxy_type_t.http
            self._settings['proxy_hostname'] = value['proxy_host'].split(':')[
                0]
            self._settings['proxy_port'] = value['proxy_host'].split(':')[1]

        # 设置 tracker urls
        if value.get('tracker_urls'):
            self._tracker_urls = value['tracker_urls']

    def _make_torrent_file(self, file_path, torrent_folder):
        """生成torrent 文件
        
        Arguments:
            file_path {string} -- 需要生成torrent的源文件
            torrent_folder {string} -- torrent保存位置
        
        Raises:
            BitTorrentError -- [description]
        """

        logger.info('[创建torrent文件] file_path: {} torrent_folder: {}'.format(
            file_path, torrent_folder))
        fs = lt.file_storage()
        lt.add_files(fs, file_path)
        if fs.num_files() == 0:
            logger.warning('-> 没有任何文件添加!')
            raise BitTorrentError(-1, '文件添加失败')

        input = os.path.abspath(file_path)
        file_name = os.path.split(file_path)[-1]
        torrent = lt.create_torrent(fs, 0, 4 * 1024 * 1024)

        logger.debug('-> tracker urls: {}'.format(self._tracker_urls))
        for tracker_url in self._tracker_urls:
            torrent.add_tracker(tracker_url)
        torrent.set_creator(CLIENT_INFO)
        try:
            lt.set_piece_hashes(torrent,
                                os.path.split(input)[0],
                                lambda x: sys.stderr.write('.'))
        except Exception as e:
            logger.warning(e)
            raise BitTorrentError(-1, 'torrent创建失败')

        torrent_file_path = self.get_torrent_file_path(file_path,
                                                       torrent_folder)
        f = open(torrent_file_path, 'wb')
        f.write(lt.bencode(torrent.generate()))
        f.close()
        logger.info('-> 保存到: {}'.format(torrent_file_path))
        return torrent_file_path

    def _destroy_torrent_file(self, torrent_file_path):
        """删除torrent文件
        
        Arguments:
            torrent_file_path {string} -- torrent 文件
        """
        os.remove(torrent_file_path)

    def _add_torrent(self, magnet_or_torrent_file_path, file_save_folder):
        """添加 磁力链接/torrent
        
        Arguments:
            magnet_or_torrent_file_path {string} -- 磁力链接/torrent文件位置
            file_save_folder {string} -- 文件所在目录
        """

        atp = {}
        # 磁力链接
        if magnet_or_torrent_file_path.startswith('magnet:'):
            atp = lt.parse_magnet_uti(magnet_or_torrent_file_path)
        # torrent种子
        else:
            torrent = lt.bdecode(
                open(magnet_or_torrent_file_path, 'rb').read())
            torrent_info = lt.torrent_info(torrent)
            atp['ti'] = torrent_info
            try:
                atp['resume_data'] = open(
                    os.path.join(file_save_folder,
                                 torrent_info.name() + '.fastresume'),
                    'rb').read()
            except:
                pass

        atp['save_path'] = file_save_folder
        atp['storage_mode'] = lt.storage_mode_t.storage_mode_sparse
        atp['paused'] = False
        atp['auto_managed'] = True
        atp['duplicate_is_error'] = True
        self._session.async_add_torrent(atp)
        # handle = self._session.add_torrent(atp)

    def _remove_torrent(self, file_path):
        """移除 torrent
        
        Arguments:
            file_path {stirng} -- 文件位置
        """
        file_path = os.path.abspath(file_path)
        for key, handle in self._torrents.iteritems():
            _file_path, _info_hash, _id = key
            # logger.debug('{} {}'.format(file_path, _file_path))
            if file_path == _file_path:
                logger.debug('remove_torrent: {} info_hash: {}'.format(file_path, _info_hash))
                self._session.remove_torrent(handle)
                break

    def _distribute_file(self, file_path, torrent_folder,
                         rebuild_torrent_file=False):
        """分发文件
        
        Arguments:
            file_path {string} -- 文件位置
            torrent_folder {string} -- torrent 保持目录
        
        Keyword Arguments:
            rebuild_torrent_file {bool} -- 如果已存在对应的torrent_file 是否重新生成 (default: {False})
        """

        logger.info('[分发文件] {}'.format(file_path))
        torrent_file_path = self.get_torrent_file_path(file_path,
                                                       torrent_folder)

        try:
            if os.path.isfile(torrent_file_path) and not rebuild_torrent_file:
                self._add_torrent(torrent_file_path,
                                  os.path.dirname(file_path))
            else:
                self._make_torrent_file(file_path, torrent_folder)
                self._add_torrent(torrent_file_path,
                                  os.path.dirname(file_path))
        except Exception as e:
            logger.error(e)

    def _redistribute_file(self, file_path, torrent_folder):
        """重新分发文件
        
        Arguments:
            file_path {string} -- 文件位置
            torrent_folder {string} -- torrent 保持目录
        """

        logger.info('[重新分发文件] {}'.format(file_path))
        torrent_file_path = self.get_torrent_file_path(file_path,
                                                       torrent_folder)

        try:
            self._remove_torrent(file_path)
            self._make_torrent_file(file_path, torrent_folder)
            self._add_torrent(torrent_file_path, os.path.dirname(file_path))
        except Exception as e:
            logger.error(e)

    def _cancle_distribute_file(self, file_path, torrent_folder):
        """取消分发文件
        
        Arguments:
            file_path {string} -- 文件位置
            torrent_folder {string} -- torrent 保持目录
        """
        logger.info('[取消分发文件] file_path: {}'.format(file_path))
        torrent_file_path = self.get_torrent_file_path(file_path,
                                                       torrent_folder)

        try:
            self._remove_torrent(file_path)
            self._destroy_torrent_file(torrent_file_path)
        except Exception as e:
            logger.error(e)

    def _monitor_resources(self):
        parent_pipe, child_pipe = Pipe(False)

        # 监听目标文件夹变化
        folder_monitor = FolderMonitor(
            **{
                'folder_category_path_dict': FOLDER_CATEGORY_PATH_DICT,
                'torrent_folder_root': TORRENT_FOLDER_ROOT,
                'pipe': child_pipe
            })
        thread = threading.Thread(
            target=folder_monitor.run_infinite, name='FOLDER_MONITOR')
        thread.start()

        while True:
            try:
                msg = parent_pipe.recv()
            except Exception as e:
                logger.error(e)
            logger.debug(msg)
            # 分发文件
            if msg['type'] == 'DISTRIBUTE_FILE':
                self._distribute_file(**msg['data'])
            # 重新分发文件
            elif msg['type'] == 'REDISTRIBUTE_FILE':
                self._redistribute_file(**msg['data'])
            # 取消分发文件
            elif msg['type'] == 'CANCLE_DISTRIBUTE_FILE':
                self._cancle_distribute_file(**msg['data'])

    def _print_status(self, status):
        out = '{:<40} [{}] '.format(status.name[:40], status.active_time)
        out += '{}: {:.2f} '.format(self.state_str[status.state],
                                    status.progress * 100)

        if status.state != lt.torrent_status.seeding:
            out += 'total_done: {} '.format(add_suffix(status.total_done))
            out += 'peers: {} seeds: {} distributed_copies: {} '.format(
                status.num_peers, status.num_seeds, status.distributed_copies)

        out += 'down: {}/s ({}) up: {}/s ({})'.format(
            add_suffix(status.download_rate),
            add_suffix(status.total_download), add_suffix(status.upload_rate),
            add_suffix(status.total_upload))

        # if status.state != lt.torrent_status.seeding:
        #     out += 'info-hash: %s\n' % status.info_hash
        #     out += 'next announce: %s\n' % status.next_announce
        #     out += 'tracker: %s\n' % status.current_tracker
        logger.info(out)

    def _print_peer_info(self, peers):
        out = '         addr            down  ( total )   up   ( total )  q  r flags      progress    client\n'

        for peer in peers:
            out += '%-15s:%-5s ' % peer.ip
            out += '%s/s ' % add_suffix(peer.down_speed)
            out += '(%s) ' % add_suffix(peer.total_download)
            out += '%s/s ' % add_suffix(peer.up_speed)
            out += '(%s) ' % add_suffix(peer.total_upload)
            out += '%2d ' % peer.download_queue_length
            out += '%2d ' % peer.upload_queue_length

            # 参考: https://www.libtorrent.org/reference-Core.html#peer_flags_t
            out += 'I' if peer.flags & lt.peer_info.interesting else '.'
            out += 'C' if peer.flags & lt.peer_info.choked else '.'
            out += 'i' if peer.flags & lt.peer_info.remote_interested else '.'
            out += 'c' if peer.flags & lt.peer_info.remote_choked else '.'
            out += 'e' if peer.flags & lt.peer_info.supports_extensions else '.'
            out += 'l' if peer.flags & lt.peer_info.local_connection else 'r'
            out += ' '

            if peer.downloading_piece_index >= 0:
                assert (peer.downloading_progress <= peer.downloading_total)
                out += '%s(%s) ' % (peer.downloading_progress,
                                    peer.downloading_total)

            if peer.flags & lt.peer_info.handshake:
                id = 'waiting for handshake'
            elif peer.flags & lt.peer_info.connecting:
                id = 'connecting to peer'
            else:
                id = peer.client

            out += '%s\n' % id[:10]

        logger.debug(out)

    def run_infinite(self):
        """
            run infinite
        """
        assert self._settings, '请先设置 settings'
        # assert self._pipe, '请先设置 pipe'

        self._session = lt.session()
        sts = lt.session_settings()
        for k, v in self._settings.iteritems():
            setattr(sts, k, v)

        if not self._net_interface:
            wan_ips = get_wan_ips()
            if wan_ips:
                self._net_interface = wan_ips[0]
            else:
                self._net_interface = '0.0.0.0'
        logger.info('监听 {}:{}'.format(self._net_interface,
                                      self._net_interface_port))
        self._session.listen_on(self._net_interface_port,
                                self._net_interface_port, self._net_interface)

        # 开启线程监听资源文件变化
        thread = threading.Thread(
            target=self._monitor_resources, name='MONITOR_RESOURCES')
        thread.start()

        # 在主线程中监听libtrorrent事件
        times = 0;
        while True:
            alerts = self._session.pop_alerts()

            # 处理 alert
            for alert in alerts:
                # tracker 错误
                if type(alert) == lt.tracker_error_alert:
                    logger.info('==> (tracker_error_alert) msg: {}'.format(
                        alert.message()))

                # 添加torrent
                elif type(alert) == lt.add_torrent_alert:
                    # 参考 https://libtorrent.org/reference-Core.html#torrent_handle
                    try:
                        # print('============== alerts ============')
                        # print_obj(alert)
                        handle = alert.handle
                        # print('============== handle ============')
                        # print_obj(handle)
                        status = handle.status()
                        # print('============== status ============')
                        # print_obj(status)
                        handle.set_max_connections(60)
                        handle.set_max_uploads(-1)

                        file_path = os.path.join(handle.save_path(), os.path.split(handle.name())[-1])
                        torrent_info_hash_str = handle.info_hash().__str__()
                        id = -1
                        try:
                            categroy = get_categroy(status.save_path)
                            res = InnerServices().BtResourceAPI.create({
                                'name': status.name,
                                'category': categroy,
                                'torrent_file_name': get_torrent_file_name(status.name),
                                'state': '{}'.format(status.state),
                                'info_hash': torrent_info_hash_str,
                                'active_time': status.active_time,
                                'announcing_to_dht': status.announcing_to_dht,
                                'announcing_to_lsd': status.announcing_to_lsd,
                                'announcing_to_trackers': status.announcing_to_trackers,
                                'is_finished': status.is_finished,
                                'is_loaded': status.is_loaded,
                                'is_seeding': status.is_seeding,
                                'progress': status.progress,
                                'completed_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(status.completed_time)),
                                'download_rate': status.download_rate,
                                'upload_rate': status.upload_rate,
                                'total_download': status.total_download,
                                'total_upload': status.total_upload,
                                'size': status.total_wanted,
                            })
                            logger.debug(res)
                            id = int(res['id'])
                        except InnerAPIError as e:
                            logger.warning(e)

                        logger.info('==> (add_torrent_alert) file: {} hash: {}'.format(
                                file_path, torrent_info_hash_str))

                        key = (file_path, torrent_info_hash_str, id)
                        self._torrents[key] = handle

                    except Exception as e:
                        logger.warning(e)
                        raise

                # 移除torrent
                elif type(alert) == lt.torrent_removed_alert:
                    # 参考 https://libtorrent.org/reference-Alerts.html#torrent_deleted_alert
                    try:
                        # print('============== alerts ============')
                        # print_obj(alert)
                        handle = alert.handle
                        # print('============== handle ============')
                        # print_obj(handle)
                        torrent_info_hash_str = alert.info_hash.__str__()
                        file_name = alert.torrent_name
                        logger.info(
                            '==> (torrent_removed_alert) file_name: {} hash: {}'.format(file_name, torrent_info_hash_str))
                        for key in self._torrents:
                            file_path, hash_str, id = key
                            if torrent_info_hash_str == hash_str:
                                self._torrents.pop(key, None)
                                if not os.path.isfile(file_path):
                                    res = InnerServices().BtResourceAPI.delete(id)
                                else:
                                    res = InnerServices().BtResourceAPI.modify(id, {'state': 'stop'})
                                break

                    except Exception as e:
                        logger.warning(e)

                # 绑定端口失败
                elif type(alert) == lt.listen_failed_alert:
                    logger.info('==> (listen_failed_alert) msg: {}'.format(
                        alert.message()))
                    sys.exit(-1)

                else:
                    logger.info('==> ({}) msg: {}'.format(
                        type(alert), alert.message()))

            # 轮询 torrent 状态
            for index, (key, handle) in enumerate(self._torrents.items()):
                file_path, torrent_info_hash_str, id = key
                # logger.debug(file_path, torrent_info_hash_str, id)
                status = handle.status()
                # print_obj(status)

                # if index == 0:
                #     logger.debug('==================== Torrents ====================')
                # self._print_status(status)

                # 更新资源状态
                try:
                    res = InnerServices().BtResourceAPI.modify(id, {
                        # 'name': status.name,
                        'state': '{}'.format(status.state),
                        # 'info_hash': torrent_info_hash_str,
                        'active_time': status.active_time,
                        'announcing_to_dht': status.announcing_to_dht,
                        'announcing_to_lsd': status.announcing_to_lsd,
                        'announcing_to_trackers': status.announcing_to_trackers,
                        # 'tracker': status.current_tracker
                        'is_finished': status.is_finished,
                        'is_loaded': status.is_loaded,
                        'is_seeding': status.is_seeding,
                        'progress': status.progress,
                        'completed_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(status.completed_time)),
                        'download_rate': status.download_rate,
                        'upload_rate': status.upload_rate,
                        # 'download_rate': random.randrange(0, 1024*6),
                        # 'upload_rate': random.randrange(0, 1024*1024*6),
                        'total_download': status.total_download,
                        'total_upload': status.total_upload,
                    })
                    # logger.debug(res)
                except InnerAPIError as e:
                    logger.warning(e)

                # if times % 5 == 0:
                #     logger.debug('+++++++++++++++++++++ {} Peers +++++++++++++++++++++'.format(status.name))
                #     self._print_peer_info(handle.get_peer_info())

                self._resource_peers[id] = []
                for peer in handle.get_peer_info():
                    peer_info = {
                        'address': peer.ip,
                        'down_speed': peer.down_speed,
                        'total_download': peer.total_download,
                        'up_speed': peer.up_speed,
                        'total_upload': peer.total_upload,
                        'download_queue_length': peer.download_queue_length,
                        'upload_queue_length': peer.upload_queue_length,
                        'flags': peer.flags,
                        'client_version': peer.client
                    }
                    self._resource_peers[id].append(peer_info)

                # # just for test
                # self._resource_peers[id] = [{
                #     'address': '156.15.54.1:89902',
                #     'down_speed': 25652,
                #     'total_download': 234354354,
                #     'up_speed': 5435234,
                #     'total_upload': 6523452345,
                #     'download_queue_length': 23445,
                #     'upload_queue_length': 5454,
                #     'flags': 1234,
                #     'client': 'btclient',
                # }, {
                #     'address': '16.5.54.1:1282',
                #     'down_speed': 652,
                #     'total_download': 2354,
                #     'up_speed': 1234,
                #     'total_upload': 52345,
                #     'download_queue_length': 45,
                #     'upload_queue_length': 54,
                #     'flags': 34,
                #     'client': 'btclient',
                # }]

                # import pprint
                # pprint.pprint(self._resource_peers)
                self._sm.json_write(self._resource_peers)

            time.sleep(5)
            times += 1
            self._resource_peers.clear()
        thread.join(3)
        logger.warning('已停止文件分发.')
